package com.luych.toolbox.automation.service.service;

import com.luych.toolbox.automation.service.constant.Constant;
import com.luych.toolbox.automation.service.convert.AutoTaskTemplateArgParamToEntity;
import com.luych.toolbox.automation.service.convert.AutoTaskTemplateNodeParamToEntity;
import com.luych.toolbox.automation.service.convert.AutoTaskTemplateNodeStdinParamToEntity;
import com.luych.toolbox.automation.service.convert.AutoTaskTemplateNodeStdoutParamToEntity;
import com.luych.toolbox.automation.service.dao.*;
import com.luych.toolbox.automation.service.entity.*;
import com.luych.toolbox.common.enums.AutoTaskTemplateNodeStdinType;
import com.luych.toolbox.common.enums.AutoTaskTemplateNodeType;
import com.luych.toolbox.common.event.base.service.publisher.WebSocketEventPublisher;
import com.luych.toolbox.common.feign.param.automation.service.*;
import com.luych.toolbox.common.feign.param.gateway.center.WebSocketEvent;
import com.luych.toolbox.common.feign.view.automation.service.*;
import com.luych.toolbox.common.page.PageParam;
import com.luych.toolbox.common.page.PageResult;
import com.luych.toolbox.common.service.base.BaseConvert;
import com.luych.toolbox.common.service.base.BaseService;
import com.luych.toolbox.common.service.exception.HttpException;
import com.luych.toolbox.common.service.page.SpringPageParamAdapter;
import com.luych.toolbox.common.service.page.SpringPageResultAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.bus.BusProperties;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.data.domain.Page;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;

@Service
public class AutoTaskTemplateService extends BaseService<AutoTaskTemplateView, AutoTaskTemplateParam, AutoTaskTemplate> {

    private static final BaseConvert<AutoTaskTemplateParam, AutoTaskTemplate> taskTemplateParamToEntity = AutoTaskTemplate::new;
    private static final BaseConvert<AutoTaskTemplate, AutoTaskTemplateView> taskTemplateEntityToView = AutoTaskTemplateView::new;
    private static final BaseConvert<AutoTaskTemplateArg, AutoTaskTemplateArgView> taskTemplateArgEntityToView = AutoTaskTemplateArgView::new;
    private static final BaseConvert<AutoTaskTemplateNode, AutoTaskTemplateNodeView> taskTemplateNodeEntityToView = AutoTaskTemplateNodeView::new;
    private static final BaseConvert<AutoTaskTemplateNodeStdin, AutoTaskTemplateNodeStdinView> taskTemplateNodeStdinEntityToView = AutoTaskTemplateNodeStdinView::new;
    private static final BaseConvert<AutoTaskTemplateNodeStdout, AutoTaskTemplateNodeStdoutView> taskTemplateNodeStdoutEntityToView = AutoTaskTemplateNodeStdoutView::new;

    private final BusProperties busProperties;
    private final ApplicationEventPublisher applicationEventPublisher;

    private final TransactionTemplate transactionTemplate;
    private final AutoTaskTemplateDao taskTemplateDao;
    private final AutoTaskTemplateArgDao taskTemplateArgDao;
    private final AutoTaskTemplateNodeDao taskTemplateNodeDao;
    private final AutoTaskTemplateNodeStdinDao taskTemplateNodeStdinDao;
    private final AutoTaskTemplateNodeStdoutDao taskTemplateNodeStdoutDao;

    @Autowired
    public AutoTaskTemplateService(BusProperties busProperties,
                                   ApplicationEventPublisher applicationEventPublisher,
                                   TransactionTemplate transactionTemplate,
                                   AutoTaskTemplateDao taskTemplateDao,
                                   AutoTaskTemplateArgDao taskTemplateArgDao,
                                   AutoTaskTemplateNodeDao taskTemplateNodeDao,
                                   AutoTaskTemplateNodeStdinDao taskTemplateNodeStdinDao,
                                   AutoTaskTemplateNodeStdoutDao taskTemplateNodeStdoutDao) {
        super(taskTemplateDao, taskTemplateParamToEntity, taskTemplateEntityToView);
        this.busProperties = busProperties;
        this.applicationEventPublisher = applicationEventPublisher;
        this.transactionTemplate = transactionTemplate;
        this.taskTemplateDao = taskTemplateDao;
        this.taskTemplateArgDao = taskTemplateArgDao;
        this.taskTemplateNodeDao = taskTemplateNodeDao;
        this.taskTemplateNodeStdinDao = taskTemplateNodeStdinDao;
        this.taskTemplateNodeStdoutDao = taskTemplateNodeStdoutDao;
    }

    @Override
    protected AutoTaskTemplateView fill(AutoTaskTemplateView view) {
        view.setArgs(taskTemplateArgDao.findByAutoTaskTemplateId(view.getId()).stream().map(taskTemplateArgEntityToView).collect(Collectors.toList()));
        view.setNodes(taskTemplateNodeDao.findByAutoTaskTemplateId(view.getId()).stream().map(taskTemplateNodeEntityToView).sorted(Comparator.comparing(AutoTaskTemplateNodeView::getNum)).collect(Collectors.toList()));
        for (AutoTaskTemplateNodeView node : view.getNodes()) {
            node.setStdin(taskTemplateNodeStdinDao.findByAutoTaskTemplateNodeId(node.getId()).stream().map(taskTemplateNodeStdinEntityToView).collect(Collectors.toList()));
            node.setStdout(taskTemplateNodeStdoutDao.findByAutoTaskTemplateNodeId(node.getId()).stream().map(taskTemplateNodeStdoutEntityToView).collect(Collectors.toList()));
        }
        return view;
    }

    public PageResult<AutoTaskTemplateView> pageByKeyword(PageParam pageParam, String keyword) {
        Page<AutoTaskTemplate> page;
        if (StringUtils.isEmpty(keyword)) {
            page = taskTemplateDao.findAll(new SpringPageParamAdapter(pageParam));
        } else {
            page = taskTemplateDao.findByNameLike('%' + keyword + '%', new SpringPageParamAdapter(pageParam));
        }
        PageResult<AutoTaskTemplate> pageResult = new SpringPageResultAdapter<>(page);
        return pageResult.convert(taskTemplateEntityToView).convert(this::fill);
    }

    private void validate(AutoTaskTemplateParam param) {
        Assert.notNull(param.getName(), "Param name can't be empty");
        Assert.notNull(param.getServerCount(), "Param serverCount can't be empty");
        Assert.isTrue(param.getServerCount() > 0, "Param serverCount must greater then 0");
        if (param.getArgs() != null) {
            for (AutoTaskTemplateArgParam argParam : param.getArgs()) {
                Assert.notNull(argParam.getId(), "Param args[x].id can't be empty");
                Assert.notNull(argParam.getType(), "Param args[x].type can't be empty");
                Assert.notNull(argParam.getName(), "Param args[x].name can't be empty");
                Assert.notNull(argParam.getDescription(), "Param args[x].description can't be empty");
            }
            Assert.isTrue(param.getArgs().size() == param.getArgs().stream().map(AutoTaskTemplateArgParam::getName).distinct().count(),
                    "Param args[x].name must different");
        }
        Assert.notEmpty(param.getNodes(), "Param nodes can't be empty");
        for (AutoTaskTemplateNodeParam nodeParam : param.getNodes()) {
            Assert.notNull(nodeParam.getId(), "Param nodes[x].id can't be empty");
            Assert.notNull(nodeParam.getName(), "Param nodes[x].name can't be empty");
            Assert.notNull(nodeParam.getType(), "Param nodes[x].type can't be empty");
            if (nodeParam.getType().equals(AutoTaskTemplateNodeType.BASH)) {
                Assert.notNull(nodeParam.getServerIndex(), "Param nodes[x].serverIndex can't be empty");
                Assert.notNull(nodeParam.getScriptId(), "Param nodes[x].scriptId can't be empty");
                if (nodeParam.getStdin() != null) {
                    for (AutoTaskTemplateNodeStdinParam stdinParam : nodeParam.getStdin()) {
                        Assert.notNull(stdinParam.getScriptArgId(), "Param nodes[x].stdin[y].scriptArgId can't be empty");
                        Assert.notNull(stdinParam.getType(), "Param nodes[x].stdin[y].type can't be empty");
                        if (stdinParam.getType().equals(AutoTaskTemplateNodeStdinType.ARG)) {
                            Assert.notNull(stdinParam.getAutoTaskTemplateArgId(), "Param nodes[x].stdin[y].autoTaskTemplateArgId can't be empty");
                            Assert.isTrue(param.getArgs().stream().map(AutoTaskTemplateArgParam::getId).anyMatch(stdinParam.getAutoTaskTemplateArgId()::equals),
                                    "Param nodes[x].stdin[y].autoTaskTemplateArgId must in param args"
                            );
                        } else if (stdinParam.getType().equals(AutoTaskTemplateNodeStdinType.INPUT)) {
                            Assert.notNull(stdinParam.getValue(), "Param nodes[x].stdin[y].value can't be empty");
                        }
                    }
                }
                Assert.notNull(nodeParam.getStdoutType(), "Param nodes[x].stdoutType can't be empty");
                if (nodeParam.getStdout() != null) {
                    for (AutoTaskTemplateNodeStdoutParam stdoutParam : nodeParam.getStdout()) {
                        Assert.notNull(stdoutParam.getAutoTaskTemplateArgId(), "Param nodes[x].stdout[y].autoTaskTemplateArgId can't be empty");
                        Assert.isTrue(param.getArgs().stream().map(AutoTaskTemplateArgParam::getId).anyMatch(stdoutParam.getAutoTaskTemplateArgId()::equals),
                                "Param nodes[x].stdout[y].autoTaskTemplateArgId must in param args"
                        );
                        Assert.notNull(stdoutParam.getExpression(), "Param nodes[x].stdout[y].expression can't be empty");
                    }
                }
            } else if (nodeParam.getType().equals(AutoTaskTemplateNodeType.SQL)) {
                Assert.notNull(nodeParam.getDataSourceId(), "Param nodes[x].dataSourceId can't be empty");
                Assert.notNull(nodeParam.getScriptId(), "Param nodes[x].scriptId can't be empty");
                if (nodeParam.getStdin() != null) {
                    for (AutoTaskTemplateNodeStdinParam stdinParam : nodeParam.getStdin()) {
                        Assert.notNull(stdinParam.getScriptArgId(), "Param nodes[x].stdin[y].scriptArgId can't be empty");
                        Assert.notNull(stdinParam.getType(), "Param nodes[x].stdin[y].type can't be empty");
                        if (stdinParam.getType().equals(AutoTaskTemplateNodeStdinType.ARG)) {
                            Assert.notNull(stdinParam.getAutoTaskTemplateArgId(), "Param nodes[x].stdin[y].autoTaskTemplateArgId can't be empty");
                            Assert.isTrue(param.getArgs().stream().map(AutoTaskTemplateArgParam::getId).noneMatch(stdinParam.getAutoTaskTemplateArgId()::equals),
                                    "Param nodes[x].stdin[y].autoTaskTemplateArgId must in param args"
                            );
                        } else if (stdinParam.getType().equals(AutoTaskTemplateNodeStdinType.INPUT)) {
                            Assert.notNull(stdinParam.getValue(), "Param nodes[x].stdin[y].value can't be empty");
                        }
                    }
                }
                if (nodeParam.getStdout() != null) {
                    for (AutoTaskTemplateNodeStdoutParam stdoutParam : nodeParam.getStdout()) {
                        Assert.notNull(stdoutParam.getAutoTaskTemplateArgId(), "Param nodes[x].stdout[y].autoTaskTemplateArgId can't be empty");
                        Assert.isTrue(param.getArgs().stream().map(AutoTaskTemplateArgParam::getId).anyMatch(stdoutParam.getAutoTaskTemplateArgId()::equals),
                                "Param nodes[x].stdout[y].autoTaskTemplateArgId must in param args"
                        );
                        Assert.notNull(stdoutParam.getExpression(), "Param nodes[x].stdout[y].expression can't be empty");
                    }
                }
            } else if (nodeParam.getType().equals(AutoTaskTemplateNodeType.FILE)) {
                Assert.notNull(nodeParam.getServerIndex(), "Param nodes[x].serverIndex can't be empty");
                Assert.notNull(nodeParam.getFileId(), "Param nodes[x].fileId can't be empty");
            }
            Assert.isTrue(param.getNodes().size() == param.getNodes().stream().map(AutoTaskTemplateNodeParam::getName).distinct().count(),
                    "Param nodes[x].name must different");
        }
    }

    @Override
    public AutoTaskTemplateView save(AutoTaskTemplateParam param) {
        if (StringUtils.isEmpty(param.getId())) {
            param.setId(UUID.randomUUID().toString());
        }

        this.validate(param);

        // check name exists
        Optional<AutoTaskTemplate> templateByNameOpt = taskTemplateDao.findByName(param.getName());
        if (templateByNameOpt.isPresent() && !templateByNameOpt.get().getId().equals(param.getId())) {
            throw new HttpException(HttpStatus.CONFLICT, Constant.TEMPLATE_SAVE_NAME_USED);
        }

        // set node num
        for (int i = 0; i < param.getNodes().size(); i++) {
            param.getNodes().get(i).setNum(i + 1);
        }

        // save
        AutoTaskTemplateView result = transactionTemplate.execute(transactionStatus -> {
            AutoTaskTemplateView view = taskTemplateEntityToView.apply(taskTemplateDao.save(taskTemplateParamToEntity.apply(param)));

            BaseService<AutoTaskTemplateArgView, AutoTaskTemplateArgParam, AutoTaskTemplateArg> argService =
                    new BaseService<>(taskTemplateArgDao, new AutoTaskTemplateArgParamToEntity(view.getId()), taskTemplateArgEntityToView);
            BaseService<AutoTaskTemplateNodeView, AutoTaskTemplateNodeParam, AutoTaskTemplateNode> nodeService =
                    new BaseService<>(taskTemplateNodeDao, new AutoTaskTemplateNodeParamToEntity(view.getId()), taskTemplateNodeEntityToView);

            view.setArgs(argService.batch(param.getArgs(), taskTemplateArgDao.findByAutoTaskTemplateId(view.getId())));
            view.setNodes(nodeService.batch(param.getNodes(), taskTemplateNodeDao.findByAutoTaskTemplateId(view.getId())));

            for (int i = 0; i < param.getNodes().size(); i++) {
                AutoTaskTemplateNodeParam nodeParam = param.getNodes().get(i);
                AutoTaskTemplateNodeView nodeView = view.getNodes().get(i);

                BaseService<AutoTaskTemplateNodeStdinView, AutoTaskTemplateNodeStdinParam, AutoTaskTemplateNodeStdin> nodeStdinService =
                        new BaseService<>(taskTemplateNodeStdinDao, new AutoTaskTemplateNodeStdinParamToEntity(nodeView.getId()), taskTemplateNodeStdinEntityToView);
                BaseService<AutoTaskTemplateNodeStdoutView, AutoTaskTemplateNodeStdoutParam, AutoTaskTemplateNodeStdout> nodeStdoutService =
                        new BaseService<>(taskTemplateNodeStdoutDao, new AutoTaskTemplateNodeStdoutParamToEntity(nodeView.getId()), taskTemplateNodeStdoutEntityToView);

                nodeView.setStdin(nodeStdinService.batch(nodeParam.getStdin(), taskTemplateNodeStdinDao.findByAutoTaskTemplateNodeId(nodeView.getId())));
                nodeView.setStdout(nodeStdoutService.batch(nodeParam.getStdout(), taskTemplateNodeStdoutDao.findByAutoTaskTemplateNodeId(nodeView.getId())));
            }

            return view;
        });

        WebSocketEventPublisher eventPublisher = new WebSocketEventPublisher(busProperties, applicationEventPublisher);
        eventPublisher.send(new WebSocketEvent(WebSocketEvent.Type.AUTO_TASK_TEMPLATE_REFRESH, "SAVE"));

        return result;
    }

    @Override
    public void remove(String id) {
        Optional<AutoTaskTemplate> template = taskTemplateDao.findById(id);
        if (!template.isPresent()) {
            throw new HttpException(HttpStatus.NOT_FOUND);
        }

        transactionTemplate.executeWithoutResult(transactionStatus -> {
            List<AutoTaskTemplateNode> nodes = taskTemplateNodeDao.findByAutoTaskTemplateId(template.get().getId());
            for (AutoTaskTemplateNode node : nodes) {
                taskTemplateNodeStdinDao.deleteByAutoTaskTemplateNodeId(node.getId());
                taskTemplateNodeStdoutDao.deleteByAutoTaskTemplateNodeId(node.getId());
            }
            taskTemplateNodeDao.deleteByAutoTaskTemplateId(template.get().getId());
            taskTemplateArgDao.deleteByAutoTaskTemplateId(template.get().getId());
            taskTemplateDao.deleteById(id);
        });
    }
}
